package com.googlecode.cswish.struts.hibernate;

import java.io.CharArrayWriter;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Future;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.servlet.ServletContext;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.struts2.views.freemarker.FreemarkerManager;
import org.hibernate.Session;
import org.hibernate.cfg.Environment;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.query.spi.QueryPlanCache;
import org.hibernate.engine.spi.Mapping;
import org.hibernate.internal.SessionFactoryImpl;
import org.hibernate.jdbc.ReturningWork;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.ForeignKey;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.Table;
import org.hibernate.service.spi.ServiceRegistryImplementor;
import org.hibernate.tool.hbm2ddl.DatabaseMetadata;
import org.hibernate.tool.hbm2ddl.ImportSqlCommandExtractor;
import org.hibernate.tool.hbm2ddl.SchemaExport;
import org.hibernate.tool.hbm2ddl.SchemaUpdate;
import org.hibernate.tool.hbm2ddl.SchemaValidator;
import org.hibernate.tool.hbm2ddl.TableMetadata;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.googlecode.cswish.Config;
import com.googlecode.cswish.annotation.ChildClass;
import com.googlecode.cswish.annotation.Protected;
import com.googlecode.cswish.model.ModelInfo;
import com.googlecode.cswish.model.PropertyInfo;
import com.googlecode.cswish.oosearch.QLInfo;
import com.googlecode.cswish.struts.ContextHolder;
import com.googlecode.cswish.struts.conversion.PageConverter;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.struts.spring.GenericService;
import com.googlecode.cswish.struts.spring.ObjectFactory;
import com.googlecode.cswish.struts.spring.SystemEnv;
import com.googlecode.cswish.util.crosssite.CrossSiteUtil;
import com.opensymphony.xwork2.inject.Inject;

import freemarker.template.Configuration;
import freemarker.template.Template;


/**
 * Dynamic model factory
 * 
 * @author Jerry.Feng Date: 2008-5-15
 */
@Service
public class DynamicModelManager {
	
	private static final Logger logger = Logger.getLogger(DynamicModelManager.class);
	
    @Resource
    private BeanIntrospector beanIntrospector;
    
    @Resource
    private HibernateConfiguration hibernateConfiguration;
    
    @Resource
	protected ObjectFactory objectFactory;
    
    @Resource
    private Config config;
    
    @Resource
	private DynamicClassLoaderFactory dynamicClassLoaderFactory;
    
    @Resource
	private GenericService genericService;
    
    @Resource
    private CrossSiteUtil crossSiteUtil;
    
    @Inject
	protected FreemarkerManager freemarkerManager;
    
    @Inject
	private ServletContext servletContext;
    
    @PersistenceContext(unitName="espirit")
    private EntityManager em;
	
    private DynamicModelManager dynamicModelManager;
	
	public DynamicModelManager getThis() {
		if (dynamicModelManager == null) {
			dynamicModelManager = objectFactory.getBean(DynamicModelManager.class, false);
		}
		return dynamicModelManager;
	}
	
	// TODO: temporary solution, don't update these system models for the technique reason
	private Collection<String> disabledSystemModels;

	@PostConstruct
	private void init() {
		//enableDynamicModel = "true".equals(System.getProperty(SystemEnv.RUNNING_IN_ECLIPSE));
		
		// FIXME: remove the code
		disabledSystemModels = new HashSet<String>();
		disabledSystemModels.add("com.espirit.eap.pagelayout.Model");
		disabledSystemModels.add("com.espirit.eap.pagelayout.DomainModel");
		disabledSystemModels.add("com.espirit.eap.pagelayout.ModelField");
		disabledSystemModels.add("com.espirit.eap.pagelayout.Element");
		disabledSystemModels.add("com.espirit.eap.pagelayout.Layout");
		disabledSystemModels.add("com.espirit.eap.pagelayout.Page");
		disabledSystemModels.add("com.espirit.eap.manager.Menu");
		disabledSystemModels.add("com.espirit.eap.sa.Privilege");
	}
	
    public boolean isEnableDynamicModel() {
		return config.isEnableSaas();			// TODO: use the separator flag?
	}

    private PageConverter pageConverter;
    private PageConverter getPageConverter() {
    	if (pageConverter == null) {
    		pageConverter = objectFactory.getBean(PageConverter.class, true);
    	}
    	return pageConverter;
    }
    
	public DynamicModelPersistent loadDefinition(String entityName, String tableName, String joinTableName) {
    	return new DynamicModelPersistent(entityName, tableName, joinTableName, beanIntrospector, 
    			getPageConverter(), hibernateConfiguration);
    }
    

	public DynamicModelPersistent checkAndLoadDefinition(String entityName, String tableName, String joinTableName) {
    	PersistentClass clazz = hibernateConfiguration.getClassMapping(entityName);
    	if (clazz == null) {
    		return null;
    	} else {
    		return new DynamicModelPersistent(entityName, tableName, joinTableName, beanIntrospector, 
    			getPageConverter(), hibernateConfiguration);
    	}
    }
    
    /**
     * add the hibernate entity definition at the runtime
     * 
     * @param hibernateClassName
     * @return
     */
    public List<Future<DynamicModelPersistent>> addHibernateDefinition(final String hibernateClassName) {
		Map<String, DynamicModelPersistent> modelDefs = new LinkedHashMap<String, DynamicModelPersistent>();
    	addHibernateDefinitionWithCheck(hibernateClassName, modelDefs);
    	
    	List<DynamicModelPersistent> defs = new ArrayList<DynamicModelPersistent>(modelDefs.values());
    	Collections.reverse(defs);
    	
    	
    	List<Future<DynamicModelPersistent>> results = new ArrayList<Future<DynamicModelPersistent>>(defs.size());
    	String site = ContextHolder.get().getSiteName();
    	for (DynamicModelPersistent dynamicModelDef : defs) {
    		dynamicModelDef.setChanged(true);
    		dynamicModelDef.commitPhase2(false);
    		
    		// TOOD: this method should be optional
    		// case 1: dynamic table
    		// case 2: load static hibernate class
    		Future<DynamicModelPersistent> future = getThis().updateDbSchemaBySite(site, dynamicModelDef, false);
    		results.add(future);
    	}
    	return results;
    }
    
    interface Callback {
    	void execute(DatabaseMetadata databaseMetadata);
    }
    
    private void addHibernateDefinitionWithCheck(String hibernateClassName, Map<String, DynamicModelPersistent> modelDefs) {
    	if (modelDefs.containsKey(hibernateClassName) || beanIntrospector.isHibernateModel(hibernateClassName)) {
    		return ;
    	}
    	
    	ModelInfo modelInfo = beanIntrospector.getModelInfo(hibernateClassName);
    	if (modelInfo != null && modelInfo.getTable() != null) {
    		// 1. check super class dependence
    		Class clazz = modelInfo.getTypeClass();
    		Class superClazz = clazz.getSuperclass();
    		if (superClazz != null && superClazz.getAnnotation(Entity.class) != null) {
    			addHibernateDefinitionWithCheck(superClazz.getName(), modelDefs);
    			
    			// check again, maybe the class is added because of the parent reference
    			// don't check 'beanIntrospector.isHibernateModel', because there maybe temporary definition
    			if (modelDefs.containsKey(hibernateClassName)) {
    				return ;
    			}
    		}
    		
    		
    		DynamicModelPersistent dynamicModelDef = 
    			loadDefinition(hibernateClassName, modelInfo.getTable(), modelInfo.getSecondTable());
    		modelDefs.put(hibernateClassName, dynamicModelDef);
    		// 2. check reference class dependence
    		boolean commitMySelf = false;
    		for (PropertyInfo propInfo : modelInfo.getDeclaredProperties()) {
    			if (propInfo.isTableColumn() && !propInfo.isSimpleObj()) {
    				if (!commitMySelf) {
    					commitMySelf = true;
    					dynamicModelDef.setChanged(true);
    					dynamicModelDef.commitPhase1(false);
    				}
    				addHibernateDefinitionWithCheck(propInfo.getGenericType(), modelDefs);
    			}
    		}
    		
    		// 3. update itself
    		dynamicModelDef.updateModelInfo(modelInfo, false, false);
    		getThis().updateDbSchema(dynamicModelDef, false);
    		
    		// 4. try to update the children model ...
    		ChildClass childClass = (ChildClass) clazz.getAnnotation(ChildClass.class);
    		if (childClass != null) {
    			for (String childClssName : childClass.value()) {
    				addHibernateDefinitionWithCheck(childClssName, modelDefs);
    			}
    		}
    	} else {
    		if (modelInfo == null) {
    			logger.error("Can't find the model definitioin, skip it: " + hibernateClassName);
    		}
    	}
    }

    public List<Future> updateHibernateModel(final List<DynamicModelInfo> dInfos, boolean forceCheckDb) {
    	return updateHibernateModel(dInfos, forceCheckDb, false);
    }
    
    /**
     * change the db structure & hibernate configuration, rebuild the modelInfo
     * @param dInfos
     */
    public List<Future> updateHibernateModel(final List<DynamicModelInfo> dInfos, boolean forceCheckDb, boolean waitResult) {
    	if (!isEnableDynamicModel() || dInfos == null || dInfos.isEmpty()) {
    		return Collections.EMPTY_LIST;
    	}
    	
    	// FIXME: special logic, don't update the system model because of some technique reason
    	for (int i = dInfos.size() - 1; i >= 0; i--) {
    		DynamicModelInfo info = dInfos.get(i);
    		if (disabledSystemModels.contains(info.getModelName())) {
    			dInfos.remove(i);
    		}
    	}
    	if (dInfos.isEmpty()) {
    		return Collections.EMPTY_LIST;
    	}
    	
    	// 1. build the hibernate information and the bean information (parent first)
    	Collections.reverse(dInfos);
    	
    	// update the hibernate model
    	List<DynamicModelPersistent> dynamicModelDefs = new ArrayList<DynamicModelPersistent>(dInfos.size());
    	List<DynamicModelInfo> dynamicModelInfos = new ArrayList<DynamicModelInfo>(dInfos.size());
    	DynamicModelManager dynamicModelManager = objectFactory.getBean(DynamicModelManager.class, false);
    	for (int i = 0, len = dInfos.size(); i < len; i++) {
    		DynamicModelInfo dynamicModelInfo = dInfos.get(i);
    		
    		String entityName = dynamicModelInfo.getModelName();
    		Map<String, DPropertyInfo> properties = dynamicModelInfo.getProperties();
    		// 1.1 update current SessionFactory and db structure, add the new properties to hibernate definition
    		DynamicModelPersistent dynamicModelDef = loadDefinition(entityName, null, null);
    		for (Entry<String, DPropertyInfo> entry : properties.entrySet()) {
    			String propertyName = entry.getKey();
    			DPropertyInfo dpropertyInfo = entry.getValue();
    			// skip it if the the modelInfo identifies it as a transient field (null means no limitation)
    			if (!dpropertyInfo.transientProperty) {
    				dynamicModelDef.addDynamicField(propertyName, dpropertyInfo.type, dpropertyInfo.genericType
    						, dpropertyInfo.length, dpropertyInfo.nullable, dpropertyInfo.unique);
    			}
    		}
    		// 1.2 remove the deleted column if there's no data existed
    		Map<String, DPropertyInfo> removedProperties = dynamicModelInfo.getRemovedProperties();
    		if (!dynamicModelDef.isNewDef() && removedProperties != null && !removedProperties.isEmpty()) {
    			ModelInfo modelInfo = beanIntrospector.getModelInfo(entityName);
    			dynamicModelManager.removeEmptyColumns(entityName, removedProperties.keySet(), dynamicModelDef, modelInfo);
    		}
    		
    		// 1.3 convert the bean info (TODO: Check flag: dynamicModelDef.isChanged() || beanIntrospector.isChanged)
    		dynamicModelDef.setChanged(true);
			dynamicModelDefs.add(dynamicModelDef);
			dynamicModelInfos.add(dynamicModelInfo);
    		
			// 1.4 reset the page conversion
			String simpleModelName = beanIntrospector.getSimpleName(entityName);
			getPageConverter().resetPage(simpleModelName);		// TODO: only reset the root entity?
    	}
    	
    	// update class information
    	Map<String, DynamicModelInfo> dynamicModelInfoMap = new HashMap<String, DynamicModelInfo>();
    	for (int i = 0, len = dInfos.size(); i < len; i++) {
    		DynamicModelInfo dynamicModelInfo = dInfos.get(i);

    		// reset bean cache & define the new model class
    		if (dynamicModelInfo.isDynamic()) {
    			dynamicModelInfoMap.put(dynamicModelInfo.getModelName(), dynamicModelInfo);
			} else {
				// the hibernate persistence still uses it, convert to static model later
				beanIntrospector.convertToStaticClass(dynamicModelInfo.getModelName());
			} 
    	}
    	beanIntrospector.convertModelToDynamicModel(dynamicModelInfoMap);
    	
    	// 2.1 commit the change, phase1 (child first)
    	ClassLoader origClassLoader = null;
    	try {
	    	Collections.reverse(dynamicModelDefs);
	    	Collections.reverse(dynamicModelInfos);
	    	for (int i = 0, len = dynamicModelDefs.size(); i < len; i++) {
	    		DynamicModelPersistent dynamicModelDef = dynamicModelDefs.get(i);
	    		
	    		DynamicModelInfo dynamicModelInfo = dynamicModelInfos.get(i);
	    		String entityName = dynamicModelInfo.getModelName();
	    		Map<String, DPropertyInfo> properties = dynamicModelInfo.getProperties();
	    		Map<String, String> theProperties = new HashMap<String, String>();
	    		for (Entry<String, DPropertyInfo> entry : properties.entrySet()) {
	    			DPropertyInfo dpropertyInfo = entry.getValue();
	    			if (!dpropertyInfo.transientProperty) {
	    				theProperties.put(entry.getKey(), dpropertyInfo.type);
	    			}
	    		}
	    		
	    		if (dynamicModelInfo.isDynamic()) {
		    		ClassLoader theLoader = dynamicClassLoaderFactory.getDynamicClassLoader(
		    				dynamicModelInfo.getModelName());
		    		origClassLoader = Thread.currentThread().getContextClassLoader();
					Thread.currentThread().setContextClassLoader(theLoader);
	    		}
	    		dynamicModelDef.commitPhase1(false);
	    	}
	    	
	    	// 2.2 comit the change, phase2 (parent first)
	    	Collections.reverse(dynamicModelDefs);
	    	Collections.reverse(dynamicModelInfos);
	    	List<File> newFiles = new ArrayList<File>();
	    	for (int i = 0, len = dynamicModelDefs.size(); i < len; i++) {
	    		DynamicModelPersistent dynamicModelDef = dynamicModelDefs.get(i);
	    		//TODO: can this operation rollback?
	    		boolean ret = dynamicModelDef.commitPhase2(false);
	    		
	    		if (ret) {
	    			if (logger.isInfoEnabled()) {
	    				DynamicModelInfo dynamicModelInfo = dynamicModelInfos.get(i);
	    				logger.info("Convert " + dynamicModelDef.getEntityName() + " to " 
	    						+ (dynamicModelInfo.isDynamic() ? "dynamic":"static")  + " model");
	    			}
	    			
	    			// 3. generate or update class and java file, it's used by the next startup
	    			DynamicModelInfo dynamicModelInfo = dynamicModelInfos.get(i);
	    			String entityName = dynamicModelInfo.getModelName();
	    			File file = outputJavaSourceFile(entityName);
	    			if (file != null) {
	    				newFiles.add(file);
	    			}
	    			
	    			// TODO: Compile the java source code to improve the performance
	    			//String sClassPath = classPath.toString().replace("%20", " ");
	    			//JavaCompilerUtil.compile(className, sourceCode, classOutputDir, sClassPath);
	    		}
	    	}
	    	
	    	for (int i = 0, len = dInfos.size(); i < len; i++) {
	    		DynamicModelInfo dynamicModelInfo = dInfos.get(i);
	    		if (!dynamicModelInfo.isDynamic()) {
	    			beanIntrospector.unloadDynamicModel(dynamicModelInfo.getModelName());
	    		}
	    	}
	    	
	    	List<Future> results = new ArrayList<Future>(dynamicModelDefs.size());
	    	String site = ContextHolder.get().getSiteName();
	    	for (int i = 0, len = dynamicModelDefs.size(); i < len; i++) {
	    		DynamicModelPersistent dynamicModelDef = dynamicModelDefs.get(i);
	    		if (forceCheckDb || dynamicModelDef.getNewProperties() != null 
	    				|| dynamicModelDef.getRemovedProperties() != null) {			// TODO: check the properties?
	    			if (waitResult) {
	    				// use the synchronize method
	    				updateDbSchemaBySite(site, dynamicModelDef, false);
	    			} else {
		    			Future result = getThis().updateDbSchemaBySite(site, dynamicModelDef, false);
		    			results.add(result);
	    			}
	    		}
	    	}
	    	
	    	// FIXME: clean the hibernate second level cache, HibernateNameAttributeExtractor etc
	    	// EhCacheRegionFactory
	    	return results;
    	} finally {
    		if (origClassLoader != null) {
    			Thread.currentThread().setContextClassLoader(origClassLoader);
    		}
    	}
    }

    @Transactional(propagation=Propagation.NOT_SUPPORTED)
    @Protected
	public void removeEmptyColumns(String entityName, Collection<String> removedProperties,
			DynamicModelPersistent dynamicModelDef, ModelInfo modelInfo) {
		for (String fieldName : removedProperties) {
			if (dynamicModelDef.getProperty(fieldName) != null) {
				String ql = "from " + entityName + " a where a." + fieldName + " is not null";
				QLInfo qlInfo = new QLInfo(ql, true);
				qlInfo.setMaxResults(1);
				boolean hasData = false;
				try {
					hasData = genericService.searchByQl(qlInfo, false).getData().size() > 0;
				} catch (Exception ex) {
					logger.warn("Failed to check empty data", ex);
					// TODO: correct? set hasData = true?
				}
				if (!hasData) {
					dynamicModelDef.removeDynamicField(fieldName);
				} else {
					PropertyInfo propertyInfo = modelInfo.getProperty(fieldName);
					String columnName = propertyInfo == null ? fieldName : propertyInfo.getColumnName();
					logger.error("Table '" + modelInfo.getTable() + '.' + columnName + "' contains data, can't remove it");
				}
			}
		}
	}
	
	private boolean hasGetMethod(Class clazz, String property) {
		try {
			clazz.getMethod("get" + StringUtils.capitalize(property), null);
			return true;
		} catch (Exception e) {
			return false;
		}
	}
	
	public String generateJavaSource(ModelInfo modelInfo, String oldSourceCode) {
		// Notice: the fullClassName maybe dynamic class name
		String fullClassName = modelInfo.getGenericType();
		int nameSuffixIndex = fullClassName.indexOf('$');
		if (nameSuffixIndex > 0) {
			fullClassName = fullClassName.substring(0, nameSuffixIndex);
		}
		int separator = fullClassName.lastIndexOf('.');
		String pack = separator == -1 ? "" : fullClassName.substring(0, separator);
		boolean isNewClass;
		String parentClassName = null;
		Class parentClass = null;
		boolean showIdProperty = true;
		try {
			Class clazz = ClassUtils.getClass(fullClassName);
			
			// parent class name
			parentClass = clazz.getSuperclass();
			if (MockObject.class.isAssignableFrom(parentClass)) {
				isNewClass = true;
				parentClass = null;
			} else {
				parentClassName = parentClass.getName();
				int index = parentClassName.lastIndexOf('.');
				String parentClassPack = index > 0 ? parentClassName.substring(0, index) : "";
				if (parentClassPack.equals(pack)) {
					parentClassName = parentClassName.substring(parentClassPack.length() + 1);		// skip the package when they're the same folder
				}
				
				// check if id existed
				showIdProperty = !hasGetMethod(clazz, "id"); // don't show id if id property existed
			}
			isNewClass = oldSourceCode == null || oldSourceCode.isEmpty();
		} catch (ClassNotFoundException ex) {
			isNewClass = true;
		}
		
		// 2) class property information & import information
		Configuration configuration = freemarkerManager.getConfiguration(servletContext);
		Map<String, Object> templateMode = new HashMap<String, Object>();
		Map<String, String> simpleNameMapping = new HashMap<String, String>();
		Map<String, String> classSimpleNameMapping = new HashMap<String, String>();
		Set<String> packs = new TreeSet<String>();
		List<PropertyInfo> propertyInfos = new ArrayList<PropertyInfo>();
		for (PropertyInfo propertyInfo : modelInfo.getAllProperties()) {
			if ("id".equals(propertyInfo.getName())) {
				continue;			// skip id
			}
			
			if (parentClass != null) {
				if (hasGetMethod(parentClass, propertyInfo.getName())) {
					continue;
				}
			}
			
			// the mapping of property type
			String classType = propertyInfo.getType();
			int index = classType.lastIndexOf('.');
			String simpleClassName;
			if (index > 0) {
				simpleClassName = classType.substring(index + 1);
			} else {
				simpleClassName = classType;
			}
			if (!simpleNameMapping.containsKey(simpleClassName)) {
				simpleNameMapping.put(simpleClassName, classType);
				classSimpleNameMapping.put(classType, simpleClassName);
				if (!classType.startsWith("java.lang.")) {
					packs.add(classType);
				}
			}
			// add property definition
			propertyInfos.add(propertyInfo);
		
			// add import definition
			if (!propertyInfo.isTableColumn()) {
				if (!packs.contains("javax.persistence.Transient")) {
					packs.add("javax.persistence.Transient");
				}
			} else if (!propertyInfo.isSimpleObj()) {
				if (!packs.contains("javax.persistence.ManyToOne")) {
					packs.add("javax.persistence.ManyToOne");
					packs.add("javax.persistence.OneToMany");
					packs.add("javax.persistence.FetchType");
				}
				
				if (propertyInfo.getTypeClass().getAnnotation(Embeddable.class) != null) {
					if (!packs.contains("javax.persistence.Embedded")) {
						packs.add("javax.persistence.Embedded");
					}
				}
			}
			
		}
		
		String simpleName = fullClassName.substring(fullClassName.lastIndexOf('.') + 1);
		ClassInfo classInfo = new ClassInfo(pack, simpleName, parentClassName, showIdProperty
				, packs, classSimpleNameMapping);
		templateMode.put("propertyInfos", propertyInfos);
		templateMode.put("classInfo", classInfo);
		
		// generate source code and compile
		String sourceCode = null;
		InputStream inputStream = null;
		Reader reader = null;
		try {
			boolean changed;
			if (isNewClass) {
				inputStream = this.getClass().getResourceAsStream("DynamicModelClass.ftl");
				reader = new InputStreamReader(inputStream);
				Template template = new Template("JavaClassBuilder", reader, configuration);
				CharArrayWriter parentCharArrayWriter = new CharArrayWriter();
				template.process(templateMode, parentCharArrayWriter);
				sourceCode = parentCharArrayWriter.toString();
				changed = true;
			} else {
				inputStream = this.getClass().getResourceAsStream("DynamicModelProperty.ftl");
				reader = new InputStreamReader(inputStream);
				Template template = new Template("JavaPropertyBuilder", reader, configuration);
				CharArrayWriter parentCharArrayWriter = new CharArrayWriter();
				List<PropertyInfo> newPropertyInfos = new ArrayList<PropertyInfo>(propertyInfos.size());
				for (PropertyInfo propertyInfo : propertyInfos) {
					String identifiedFlag = "public " + classInfo.getSimpleType(propertyInfo.getType())
						+ " get" + classInfo.capitalize(propertyInfo.getName()) + "()";
					if (oldSourceCode.indexOf(identifiedFlag) == -1) {
						newPropertyInfos.add(propertyInfo);
					}
				}
				
				if (newPropertyInfos.size() > 0) {
					templateMode.put("propertyInfos", newPropertyInfos);
					classInfo.setClassSimpleNameMapping(Collections.EMPTY_MAP);
					
					template.process(templateMode, parentCharArrayWriter);
					sourceCode = parentCharArrayWriter.toString();
					
					int index = oldSourceCode.lastIndexOf('}');
					sourceCode = oldSourceCode.substring(0, index) + sourceCode + oldSourceCode.substring(index);
					changed = true;
				} else {
					changed = false;
				}
			}
			
			if (!changed) {
				sourceCode = oldSourceCode;
			}
		} catch (Exception ex) {
			logger.error("Failed to create java file", ex);
		} finally {
			IOUtils.closeQuietly(reader);
			IOUtils.closeQuietly(inputStream);
		}
		return sourceCode;
	}
	
	private File outputJavaSourceFile(String entityName) {
		if (freemarkerManager == null) {
			objectFactory.injectInternalBeans(this);
		}
		String sourceDir = config.getJavaSourceDir();
		String webRootDir = config.getRealWebRootPath();
		String classOutputDir = webRootDir
				+ File.separatorChar + "WEB-INF/classes/".replace('/', File.separatorChar);
		File classOutputDirFile = new File(classOutputDir);
		if (!classOutputDirFile.exists()) {
			classOutputDirFile.mkdirs();
		}
		
		// build template model
		// 1) class name information
		File sourceCodeFile = new File(sourceDir + entityName.replace('.', File.separatorChar) + ".java");
		ModelInfo modelInfo = beanIntrospector.getModelInfo(entityName);
		try {
			String oldSourceCode = sourceCodeFile.exists() ? FileUtils.readFileToString(sourceCodeFile, "UTF-8") : null;
			String sourceCode = generateJavaSource(modelInfo, oldSourceCode);
			
			if (sourceCode != oldSourceCode) { // file content changed
				FileUtils.writeStringToFile(sourceCodeFile, sourceCode, "UTF-8");
				return sourceCodeFile;
			}
		} catch (Exception ex) {
			logger.error("Failed to create java file", ex);
		}
		return null;
	}
	
	public class ClassInfo {
		
		private Collection<String> importPackages;
		
		private String pack;
		
		private String className;
		
		private String parentClassName;
		
		private boolean showIdProperty;
		
		private Map<String, String> classSimpleNameMapping;	// using by the property type, full class name --> simple name
		
		public ClassInfo(String pack, String className, String parentClassName, boolean showIdProperty, 
				Collection<String> importPackages, Map<String, String> classSimpleNameMapping) {
			
			this.pack = pack;
			this.className = className;
			this.parentClassName = parentClassName;
			this.showIdProperty = showIdProperty;
			this.importPackages = importPackages;
			this.classSimpleNameMapping = classSimpleNameMapping;
		}
		
		public String getParentClassName() {
			return parentClassName;
		}

		public boolean isShowIdProperty() {
			return showIdProperty;
		}

		public String getPack() {
			return pack;
		}

		public String getClassName() {
			return className;
		}
		
		public boolean isList(PropertyInfo propertyInfo) {
			return propertyInfo.getType().equals(List.class.getName());
		}
		
		public boolean isEmbeddable(PropertyInfo propertyInfo) {
			return propertyInfo.getTypeClass().getAnnotation(Embeddable.class) != null;
		}
		
		public Map<String, String> getClassSimpleNameMapping() {
			return classSimpleNameMapping;
		}

		public void setClassSimpleNameMapping(Map<String, String> classSimpleNameMapping) {
			this.classSimpleNameMapping = classSimpleNameMapping;
		}

		public Collection<String> getImportPackages() {
			return importPackages;
		}
		
		public String getSimpleType(String type) {
			String simpleName = classSimpleNameMapping.get(type);
			if (simpleName == null) {
				if (type.startsWith("java.lang.")) {
					return type.substring("java.lang.".length());
				} else {
					return type;
				}
			} else {
				return simpleName;
			}
		}
		
		public String getMappedBy(PropertyInfo propertyInfo) {
			String mappedBy = this.uncapitalize(ModelInfo.getStaticClassName(className));
			ModelInfo modelInfo = beanIntrospector.getModelInfo(propertyInfo.getGenericType());
			if (modelInfo.getProperty(mappedBy) == null) {
				String fullClassName = pack + '.' + className;
				Class clazz = null;
				try {
					clazz = ClassUtils.getClass(fullClassName);
				} catch (ClassNotFoundException e) {
					logger.error("Failed to load class:" + fullClassName, e);
				}
				boolean found = false;
				if (clazz != null) {
					// it doesn't use the default configuration, scan all the property to find the reference 
					for (PropertyInfo info : modelInfo.getAllProperties()) {
						if (info.isTableColumn() && info.getTypeClass().isAssignableFrom(clazz)) {
							mappedBy = info.getName();
							found = true;
							break;
						}
					}
				}
				
				if (!found) {	// maybe, the class is still not generated
					// FIXME: Is there better way to get the mapped property?
					logger.error("Don't find the 'mappedBy' property for " + className + '.' + propertyInfo.getName());
				}
			}
			
			return mappedBy;
		}
		
		public String capitalize(String str) {
			return StringUtils.capitalize(str);
		}
		
		public String uncapitalize(String str) {
			return StringUtils.uncapitalize(str);
		}
	}
	

	@Async
    //@see Configuration.generateSchemaUpdateScript & SchemaUpdate.execute
    public Future<DynamicModelPersistent> updateDbSchemaBySite(String site, DynamicModelPersistent dynamicModelDef, boolean isDrop) {
		ContextHolder context = ContextHolder.get();
		String oldSiteName = context.getSiteName();
		try {
			context.setSiteName(site);
			ObjectFactory.getInstance().getBean(DynamicModelManager.class, false)
					.updateDbSchema(dynamicModelDef, isDrop);
		} finally {
			context.setSiteName(oldSiteName);
		}
    	return new AsyncResult<DynamicModelPersistent>(dynamicModelDef);
	}
    
	@Transactional(propagation=Propagation.REQUIRES_NEW)
	public void updateDbSchema(final DynamicModelPersistent dynamicModelDef, final boolean isDrop) {
		Session session = (Session)em.getDelegate();
		Boolean ret = session.doReturningWork(new ReturningWork<Boolean>() {
			@Override
			public Boolean execute(Connection connection) throws SQLException {
				connection.setAutoCommit(false); 
				updateDbSchema(dynamicModelDef, connection, isDrop);
				connection.commit(); 
		    	return Boolean.TRUE;
			}
    	});
	}
	
    private void updateDbSchema(DynamicModelPersistent dynamicModelDef, Connection connection, boolean isDrop) {
    	ContextHolder contextHolder = ContextHolder.get();
    	String siteName = contextHolder.getSiteName().intern();
    	synchronized (siteName) {
	    	org.hibernate.cfg.Configuration config = null;
			String ddlValue = null;
			try {
				List<org.hibernate.cfg.Configuration> configs = hibernateConfiguration.getConfigurations();
				config = configs.iterator().next();
				ddlValue = config.getProperty("hibernate.hbm2ddl.auto");
				config.setProperty("hibernate.hbm2ddl.auto", "create-drop");
				Mapping mapping = config.buildMapping();
				Properties properties = config.getProperties();
				
				Dialect dialect = Dialect.getDialect(properties);
				String entityName = dynamicModelDef.getEntityName();
				Table table = dynamicModelDef.getPersistentClass(entityName).getTable();
				String defaultCatalog = properties.getProperty( Environment.DEFAULT_CATALOG );
				String defaultSchema = properties.getProperty( Environment.DEFAULT_SCHEMA );
				
				DatabaseMetadata databaseMetadata = new DatabaseMetadata(connection, dialect, config);
				TableMetadata tableInfo = databaseMetadata.getTableMetadata(
						table.getName(),
						( table.getSchema() == null ) ? defaultSchema : table.getSchema(),
						( table.getCatalog() == null ) ? defaultCatalog : table.getCatalog(),
								table.isQuoted()
				);
						
				boolean hasUpdateSql = false;
				if (isDrop) {
					if (tableInfo != null) {
						String sql = table.sqlDropString(dialect, defaultCatalog, defaultSchema);
						hasUpdateSql = true;
						Statement stmt = connection.createStatement();
						stmt.executeUpdate(sql);
						closeStmt(stmt);
						logger.info(sql);
					}
				} else {
					if (tableInfo == null) {					
						// create a new table
						String sqlCreateTable = table.sqlCreateString(dialect, mapping, defaultCatalog, defaultSchema);
						hasUpdateSql = true;
						Statement stmtCreateTable = connection.createStatement();
						stmtCreateTable.executeUpdate(sqlCreateTable);
						closeStmt(stmtCreateTable);
						logger.info(sqlCreateTable);
						
						Iterator<ForeignKey> iter = table.getForeignKeyIterator();
						while (iter.hasNext()) {
							ForeignKey fk = iter.next();
							Table referencedTable = hibernateConfiguration.getClassMapping(fk.getReferencedEntityName()).getTable();
							fk.setReferencedTable(referencedTable);
							String sql = fk.sqlCreateString(dialect, mapping, defaultCatalog, defaultSchema);
							Statement stmt = connection.createStatement();
							stmt.executeUpdate(sql);
							closeStmt(stmt);
							logger.info(sql);
						}
					} else {
						// remove column
						Collection<Property> removedProperties = dynamicModelDef.getRemovedProperties();
						if (removedProperties != null) {
							hasUpdateSql = removedProperties.size() > 0;
							
							String tableName = tableInfo.getName();
							// create the drop column sql
							for (Property property : removedProperties) {
								Object item = property.getColumnIterator().next();
								if (item instanceof Column) {
									Column column = (Column)item;
									String columnName = column.getName();
									String sql = "ALTER TABLE " + tableName + " DROP COLUMN " + columnName;
									Statement stmt = connection.createStatement();
									stmt.executeUpdate(sql);
									closeStmt(stmt);
									logger.info(sql);
								}
							}
							removedProperties = null;
						}
						
						// new column
						Iterator<String> iter = table.sqlAlterStrings(dialect, mapping, tableInfo, defaultCatalog, defaultSchema);
						hasUpdateSql = iter.hasNext();
						while (iter.hasNext()) {
							String sql = iter.next();
							Statement stmt = connection.createStatement();
							stmt.executeUpdate(sql);
							closeStmt(stmt);
							logger.info(sql);
						}
					}
				}
				if (hasUpdateSql) {
					if (logger.isDebugEnabled()) {
						logger.debug("Update DB schema successfully: " + dynamicModelDef.getEntityName());
					}
				}
			} catch (Exception ex) {
				logger.error("Update DB Schema: " + dynamicModelDef.getEntityName(), ex);
			} finally {
				if (config != null && ddlValue != null) {
					config.setProperty("hibernate.hbm2ddl.auto", ddlValue);
				}
			}
    	}
    }
    
    private void closeStmt(Statement stmt) {
    	try {
			if (stmt != null ) {
				stmt.close();
			}
		}
		catch ( Exception e ) {
			logger.error( "Error closing statement 1", e );
		}
    }

	public void enableCrossSite() {
		if (!isEnableDynamicModel()) {
			return;
		}
		
		List<Class> enabledClass = new ArrayList<Class>();
		enabledClass.add(PersistentClass.class);
		enabledClass.add(Table.class);
		for (org.hibernate.cfg.Configuration cfg : hibernateConfiguration.getConfigurations()) {
			crossSiteUtil.convertToCrossSiteInstance(cfg, enabledClass);
		}
		
		// SessionFactoryImpl.entityPersisters
		// SessionFactoryImpl.queryPlanCache.queryPlanCache is BoundedConcurrentHashMap
		enabledClass = new ArrayList<Class>();
		//enabledClass.add(EntityPersister.class);
		//enabledClass.add(CollectionPersister.class);
		enabledClass.add(QueryPlanCache.class);
		SessionFactoryImpl sessionFactory = (SessionFactoryImpl)hibernateConfiguration.getSessionFactory();
		crossSiteUtil.convertToCrossSiteInstance(sessionFactory, enabledClass);
	}
	
	public void refreshDbSchema(boolean autoCreateSchema, boolean autoUpdateSchema, 
			boolean autoDropSchema, boolean autoValidateSchema) {
		SessionFactoryImpl sessionFactory = (SessionFactoryImpl)hibernateConfiguration.getSessionFactory();
		ServiceRegistryImplementor serviceRegistry = sessionFactory.getServiceRegistry();
		org.hibernate.cfg.Configuration cfg = hibernateConfiguration.getConfigurations().get(0);
		if (autoCreateSchema) {
			new SchemaExport( serviceRegistry, cfg )
					.setImportSqlCommandExtractor( serviceRegistry.getService( ImportSqlCommandExtractor.class ) )
					.create( false, true );
		}
		if (autoUpdateSchema) {
			new SchemaUpdate( serviceRegistry, cfg ).execute( false, true );
		}
		if (autoValidateSchema) {
			new SchemaValidator( serviceRegistry, cfg ).validate();
		}
		if (autoDropSchema) {
			SchemaExport schemaExport = new SchemaExport( serviceRegistry, cfg )
					.setImportSqlCommandExtractor( serviceRegistry.getService( ImportSqlCommandExtractor.class ) );
			schemaExport.drop( false, true );
		}
	}
	
	public void refreshDbSchema() {
		if (!config.isEnableSaas()) {
			return;
		}
		
		if (logger.isDebugEnabled()) {
			logger.debug("refreshDbSchema, the target site is " + ContextHolder.get().getSiteName());
		}
		
		// see SettingsFactory.buildSettings, AvailableSettings.HBM2DDL_AUTO
		org.hibernate.cfg.Configuration cfg = hibernateConfiguration.getConfigurations().get(0);
		String autoSchemaExport = cfg.getProperty("cswish.hbm2ddl.auto");
		boolean autoCreateSchema = false;
		boolean autoUpdateSchema = false;
		boolean autoDropSchema = false;
		boolean autoValidateSchema = false;
		
		if ("validate".equals(autoSchemaExport)) {
			autoValidateSchema = true;
		} else if ("update".equals(autoSchemaExport)) {
			autoUpdateSchema = true;
		} else if ("create".equals(autoSchemaExport)) {
			if ("true".equals(System.getProperty(SystemEnv.RUNNING_IN_ECLIPSE))) {
				autoCreateSchema = true;
			} else {
				logger.warn("Don't enable auto creat in the product environment, switch it to auto update");
				autoUpdateSchema = true;
			}
		} else if ("create-drop".equals(autoSchemaExport)) {
			if ("true".equals(System.getProperty(SystemEnv.RUNNING_IN_ECLIPSE))) {
				autoCreateSchema = true;
				autoUpdateSchema = true;
				autoDropSchema = true;
			} else {
				logger.warn("Don't enable auto create/drop in the product environment, switch it to auto update");
				autoUpdateSchema = true;
			}
		}
		
		refreshDbSchema(false, autoUpdateSchema, false, autoValidateSchema);
	}
}